/**
 * Copyright (C) 2011 Whisper Systems
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.appspot.eusms.service;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;

import com.appspot.eusms.crypto.InvalidPassphraseException;
import com.appspot.eusms.crypto.MasterSecretUtil;
import com.appspot.eusms.database.CanonicalSessionMigrator;
import com.appspot.eusms.util.TextSecurePreferences;
import com.appspot.eusms.util.WorkerThread;

import org.whispersystems.textsecure.crypto.MasterSecret;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * Services that handles sending/receiving of SMS/MMS.
 *
 * @author Moxie Marlinspike
 */

public class SendReceiveService extends Service {

    public static final String SEND_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_SMS_ACTION";
    public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
    public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION";
    public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION";
    public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION";
    public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION";
    public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION";
    public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION";
    public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION";
    public static final String RECEIVE_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_ACTION";
    public static final String DECRYPTED_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DECRYPTED_PUSH_ACTION";
    public static final String DOWNLOAD_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_PUSH_ACTION";
    public static final String DOWNLOAD_AVATAR_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_AVATAR_ACTION";

    public static final String MASTER_SECRET_EXTRA = "master_secret";

    private static final int SEND_SMS = 0;
    private static final int RECEIVE_SMS = 1;
    private static final int SEND_MMS = 2;
    private static final int RECEIVE_MMS = 3;
    private static final int DOWNLOAD_MMS = 4;
    private static final int DOWNLOAD_MMS_PENDING = 5;
    private static final int RECEIVE_PUSH = 6;
    private static final int DOWNLOAD_PUSH = 7;
    private static final int DOWNLOAD_AVATAR = 8;

    private ToastHandler toastHandler;
    private SystemStateListener systemStateListener;

    private SmsReceiver smsReceiver;
    private SmsSender smsSender;
    private MmsReceiver mmsReceiver;
    private MmsSender mmsSender;
    private MmsDownloader mmsDownloader;
    private PushReceiver pushReceiver;
    private PushDownloader pushDownloader;
    private AvatarDownloader avatarDownloader;

    private MasterSecret masterSecret;
    private boolean hasSecret;

    private NewKeyReceiver newKeyReceiver;
    private ClearKeyReceiver clearKeyReceiver;
    private List<Runnable> workQueue;
    private List<Runnable> pendingSecretList;

    @Override
    public void onCreate() {
        initializeHandlers();
        initializeProcessors();
        initializeAddressCanonicalization();
        initializeWorkQueue();
        initializeMasterSecret();
    }

    @Override
    public void onStart(Intent intent, int startId) {
        if (intent == null) return;

        String action = intent.getAction();

        if (action.equals(SEND_SMS_ACTION))
            scheduleSecretRequiredIntent(SEND_SMS, intent);
        else if (action.equals(RECEIVE_SMS_ACTION))
            scheduleIntent(RECEIVE_SMS, intent);
        else if (action.equals(SENT_SMS_ACTION))
            scheduleIntent(SEND_SMS, intent);
        else if (action.equals(DELIVERED_SMS_ACTION))
            scheduleIntent(SEND_SMS, intent);
        else if (action.equals(SEND_MMS_ACTION))
            scheduleSecretRequiredIntent(SEND_MMS, intent);
        else if (action.equals(RECEIVE_MMS_ACTION))
            scheduleIntent(RECEIVE_MMS, intent);
        else if (action.equals(DOWNLOAD_MMS_ACTION))
            scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
        else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION))
            scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent);
        else if (action.equals(RECEIVE_PUSH_ACTION))
            scheduleIntent(RECEIVE_PUSH, intent);
        else if (action.equals(DECRYPTED_PUSH_ACTION))
            scheduleSecretRequiredIntent(RECEIVE_PUSH, intent);
        else if (action.equals(DOWNLOAD_PUSH_ACTION))
            scheduleSecretRequiredIntent(DOWNLOAD_PUSH, intent);
        else if (action.equals(DOWNLOAD_AVATAR_ACTION))
            scheduleIntent(DOWNLOAD_AVATAR, intent);
        else
            Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        Log.w("SendReceiveService", "onDestroy()...");
        super.onDestroy();

        if (newKeyReceiver != null)
            unregisterReceiver(newKeyReceiver);

        if (clearKeyReceiver != null)
            unregisterReceiver(clearKeyReceiver);
    }

    private void initializeHandlers() {
        systemStateListener = new SystemStateListener(this);
        toastHandler = new ToastHandler();
    }

    private void initializeProcessors() {
        smsReceiver = new SmsReceiver(this);
        smsSender = new SmsSender(this, systemStateListener, toastHandler);
        mmsReceiver = new MmsReceiver(this);
        mmsSender = new MmsSender(this, systemStateListener, toastHandler);
        mmsDownloader = new MmsDownloader(this, toastHandler);
        pushReceiver = new PushReceiver(this);
        pushDownloader = new PushDownloader(this);
        avatarDownloader = new AvatarDownloader(this);
    }

    private void initializeWorkQueue() {
        pendingSecretList = new LinkedList<Runnable>();
        workQueue = new LinkedList<Runnable>();

        Thread workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
        workerThread.start();
    }

    private void initializeMasterSecret() {
        hasSecret = false;
        newKeyReceiver = new NewKeyReceiver();
        clearKeyReceiver = new ClearKeyReceiver();

        IntentFilter newKeyFilter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT);
        registerReceiver(newKeyReceiver, newKeyFilter, KeyCachingService.KEY_PERMISSION, null);

        IntentFilter clearKeyFilter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
        registerReceiver(clearKeyReceiver, clearKeyFilter, KeyCachingService.KEY_PERMISSION, null);

        Intent bindIntent = new Intent(this, KeyCachingService.class);
        startService(bindIntent);
        bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private void initializeWithMasterSecret(MasterSecret masterSecret) {
        Log.w("SendReceiveService", "SendReceive service got master secret.");

        if (masterSecret != null) {
            synchronized (workQueue) {
                this.masterSecret = masterSecret;
                this.hasSecret = true;

                Iterator<Runnable> iterator = pendingSecretList.iterator();
                while (iterator.hasNext())
                    workQueue.add(iterator.next());

                workQueue.notifyAll();
            }
        }
    }

    private void initializeAddressCanonicalization() {
        CanonicalSessionMigrator.migrateSessions(this);
    }

    private MasterSecret getPlaceholderSecret() {
        try {
            return MasterSecretUtil.getMasterSecret(SendReceiveService.this,
                    MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
        } catch (InvalidPassphraseException e) {
            Log.w("SendReceiveService", e);
            return null;
        }
    }

    private void scheduleIntent(int what, Intent intent) {
        Runnable work = new SendReceiveWorkItem(intent, what);

        synchronized (workQueue) {
            workQueue.add(work);
            workQueue.notifyAll();
        }
    }

    private void scheduleSecretRequiredIntent(int what, Intent intent) {
        Runnable work = new SendReceiveWorkItem(intent, what);

        synchronized (workQueue) {
            if (!hasSecret && TextSecurePreferences.isPasswordDisabled(SendReceiveService.this)) {
                initializeWithMasterSecret(getPlaceholderSecret());
            }

            if (hasSecret) {
                workQueue.add(work);
                workQueue.notifyAll();
            } else {
                pendingSecretList.add(work);
            }
        }
    }

    private class SendReceiveWorkItem implements Runnable {
        private final Intent intent;
        private final int what;

        public SendReceiveWorkItem(Intent intent, int what) {
            this.intent = intent;
            this.what = what;
        }

        @Override
        public void run() {
            MasterSecret masterSecret = SendReceiveService.this.masterSecret;

            if (masterSecret == null && TextSecurePreferences.isPasswordDisabled(SendReceiveService.this)) {
                masterSecret = getPlaceholderSecret();
            }

            switch (what) {
                case RECEIVE_SMS:
                    smsReceiver.process(masterSecret, intent);
                    return;
                case SEND_SMS:
                    smsSender.process(masterSecret, intent);
                    return;
                case RECEIVE_MMS:
                    mmsReceiver.process(masterSecret, intent);
                    return;
                case SEND_MMS:
                    mmsSender.process(masterSecret, intent);
                    return;
                case DOWNLOAD_MMS:
                    mmsDownloader.process(masterSecret, intent);
                    return;
                case DOWNLOAD_MMS_PENDING:
                    mmsDownloader.process(masterSecret, intent);
                    return;
                case RECEIVE_PUSH:
                    pushReceiver.process(masterSecret, intent);
                    return;
                case DOWNLOAD_PUSH:
                    pushDownloader.process(masterSecret, intent);
                    return;
                case DOWNLOAD_AVATAR:
                    avatarDownloader.process(masterSecret, intent);
                    return;
            }
        }
    }

    public class ToastHandler extends Handler {
        public void makeToast(String toast) {
            Message message = this.obtainMessage();
            message.obj = toast;
            this.sendMessage(message);
        }

        @Override
        public void handleMessage(Message message) {
            Toast.makeText(SendReceiveService.this, (String) message.obj, Toast.LENGTH_LONG).show();
        }
    }

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder) service).getService();
            MasterSecret masterSecret = keyCachingService.getMasterSecret();

            initializeWithMasterSecret(masterSecret);

            SendReceiveService.this.unbindService(this);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };

    private class NewKeyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.w("SendReceiveService", "Got a MasterSecret broadcast...");
            initializeWithMasterSecret((MasterSecret) intent.getParcelableExtra(MASTER_SECRET_EXTRA));
        }
    }

    /**
     * This class receives broadcast notifications to clear the MasterSecret.
     * <p/>
     * We don't want to clear it immediately, since there are potentially jobs
     * in the work queue which require the master secret.  Instead, we reset a
     * flag so that new incoming jobs will be evaluated as if no mastersecret is
     * present.
     * <p/>
     * Then, we add a job to the end of the queue which actually clears the masterSecret
     * value.  That way all jobs before this moment will be processed correctly, and all
     * jobs after this moment will be evaluated as if no mastersecret is present (and potentially
     * held).
     * <p/>
     * When we go to actually clear the mastersecret, we ensure that the flag is still false.
     * This allows a new mastersecret broadcast to come in correctly without us clobbering it.
     */
    private class ClearKeyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.w("SendReceiveService", "Got a clear mastersecret broadcast...");

            synchronized (workQueue) {
                SendReceiveService.this.hasSecret = false;
                workQueue.add(new Runnable() {
                    @Override
                    public void run() {
                        Log.w("SendReceiveService", "Running clear key work item...");

                        synchronized (workQueue) {
                            if (!SendReceiveService.this.hasSecret) {
                                Log.w("SendReceiveService", "Actually clearing key...");
                                SendReceiveService.this.masterSecret = null;
                            }
                        }
                    }
                });

                workQueue.notifyAll();
            }
        }
    }

    ;
}
